package compiler.common;

import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;

/**
 * Type class represents a type description in JKlasy language
 * @author Michal Anglart
 *
 */
public class TypeTC {  
  private String name;                       // name of type
  private List<Method> methods;              // method list
  private Map<String, String> publicFields;  // mapping from public field names to types
  public boolean hasDefaultInitImpl;         // true if there exist default 
                                             //   constructor implentation 
  
  /**
   * Standard constructor inits object with type name
   * @param name Name of type
   */
  public TypeTC(String name) {
    this.name = name;
    this.methods = new ArrayList<Method>();
    this.publicFields = new HashMap<String, String>();
    this.hasDefaultInitImpl = true;
  }
  
  public Map<String, String> makeMethodSig() {
    HashMap<String, String> map = new HashMap<String, String>();
    String key = "";
    String val = "";
    
    for(Method method : this.methods) {
      key = this.name + "$" + method.getName() + "$" + method.arity();
      val = method.getEmiterSignature(this.name, false);
      map.put(key, val);
    }
    return map;
  }
  
  /**
   * Makes new type: array of this objects
   * @return Array of this type
   */
  public TypeTC makeArrayType() {
    TypeTC array = new TypeTC("array of " + this.name);
    return array;
  }
  
  /**
   * Checks if all declared method in this type has its implementation
   * @return True if so, false otherwise 
   */
  public boolean checkImplementations() {
    boolean result = true;
    for(Method m : this.methods) {
      result = result && m.getImplementation();
    }
    return result;
  }
  
  /**
   * This method looks for specified method (via it's name, 
   * argument types, and return type. And sets implementation flag to true
   * (means that this method has implementation in class).
   * @param methodname Name of method
   * @param argtypes List of arguments types
   * @param rettype Return type
   * @throws Exception Thrown when implementation flag is already set
   */
  public void setImplementation(String methodname, 
      List<String> argtypes, String rettype) throws Exception {
    try {
      for(Method m : this.methods) {
        if(m.getName().equals(methodname) && m.getArgTypes().equals(argtypes) && 
            m.getRetType().equals(rettype))
          m.setImplementation(true);
      }
    }
    catch (Exception e) {
      throw new Exception(e.getMessage());
    }
  }
  
  /**
   * This method adds default constructor (without params) if it is
   * not implemented in class
   */
  public void makeDefaultConstructor()
  {
    for(Method m : this.methods) {
      if((m.arity() == 0) && (this.name.equals(m.getName()))) {
        return;
      }
    }
    this.hasDefaultInitImpl = false;
    Method method = new Method(this.name);
    method.setModifier(true);
    try {
      method.setImplementation(true);
      this.addMethod(method);
    }
    catch(Exception e) {
    }
  }
  
  /**
   * Gets type name
   * @return type name
   */
  public String getName() {
    return this.name;
  }
  
  /**
   * Adds method to type description
   * @param method to add
   * @throws Exception This exception is thrown when method is already defined
   */
  public void addMethod(Method method) throws Exception {
    for(Method m : this.methods) {
      if(method.equals(m))
        throw new Exception("Error: redefined method '" + method.getName() + 
              "' in '" + this.name + "' class");
    }
    List<String> args = method.getArgTypes();
    String name1 = method.getName();
    if(method.getOperator())
    {
      if((name1.equals("<")  || name1.equals(">")   || 
          name1.equals("<=") || name1.equals(">=")  ||
          name1.equals("=")  || name1.equals("!=")) &&
          method.arity() == 2 &&
          method.getRetType().equals("boolean")   &&
          args.get(0).equals(this.name)           &&
          args.get(1).equals(this.name)
         )
        this.methods.add(method);
      else if((name1.equals("+")  || name1.equals("-")   || 
          name1.equals("*") || name1.equals("div")  ||
          name1.equals("mod")) && 
          method.arity() == 2 &&
          method.getRetType().equals(this.name)   &&
          args.get(0).equals(this.name)           &&
          args.get(1).equals(this.name)
         )
        this.methods.add(method);
      else
        throw new Exception("Bad types for operator '" + name1 + "'. " +
        		"It should be (" + this.name + "," + this.name + ")=>boolean for" +
        		" relational operators, and (" + this.name + "," + this.name + ")=>" +
        		this.name + " for +,-,*,div,mod.");        
    }
    else
      this.methods.add(method);
  }
  
  /**
   * Adds public field to type description
   * @param fieldname
   * @throws Exception
   */
  public void addField(String fieldname, String fieldtype) throws Exception {
    if(this.hasField(fieldname))
      throw new Exception("Error: redefined public field '" + fieldname + 
              "' in '" + this.name + "' class");
    else
      this.publicFields.put(fieldname, fieldtype);
  }
  
  /**
   * Checks if type has specified field visible as public
   * @param fieldname
   * @return
   */
  public boolean hasField(String fieldname) {
    return this.publicFields.containsKey(fieldname);
  }
  
  /**
   * Checks if type implements specified method
   * @param methodname
   * @return true iff method is implemented by this type
   */
  public boolean hasMethod(String methodname, List<String> argtypes) {
    for(Method m : methods) {
      if(m.isEqual(methodname, argtypes));
        return true;
    }
    return false;
  }
  
  /**
   * Gets return type of specified method (via its name, and argument types)
   * @param method Method name
   * @param argtypes Arguments types
   * @return Return type
   * @throws Exception thrown when method doesn't exist
   */
  public String getMethodReturnType(String method, 
      List<String> argtypes, boolean checkAccessor) throws Exception {
    for(Method m : methods) {
      if(m.isEqual(method, argtypes)) {
        if(checkAccessor && !m.getModifier())
          throw new Exception("Trying to access private method '" + method + "'");
        else
          return m.getRetType();
      }        
    }
    String t = "(";
    for(String types : argtypes) {
      t += types;
      t += ", ";
    }
    if(t.length() > 1)
      t = t.substring(0, t.length() -2);
    t += ")";
    throw new Exception("Method '" + method + "' with argument types " + t + " doesn't exist in type '" + this.name + "'");
  }
  
  public String methodReturnType(String method, int ar) {
    for(Method m : this.methods) {
      if(m.arity() == ar && method.equals(m.getName()))
        return m.getRetType();
    }
    return null;
  }
  
  /**
   * Gets list of method argument types (method is recognized by its name and arity)
   * @param method Method name
   * @param arity Arity of method
   * @return List of types
   * @throws Exception Thrown when method is undeclared
   */
  public List<String> getMethodArgTypes(String method, int arity) throws Exception {
    for(Method m : methods) {
      if(method.equals(m.getName()) && m.arity() == arity)
        return m.getArgTypes();
    }
    throw new Exception("Method '" + method + "' undeclared");
  }
  
  /**
   * Gets type of specified field
   * @param fieldname
   * @return
   */
  public String getFieldType(String fieldname) {
    return this.publicFields.get(fieldname);
  }

  /**
   * Checks if this is given type
   * @param typename
   * @return
   */
  public boolean isEqual(String typename) {
    return typename.equals(this.name);
  }
}
